Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 5590475bf109008743f62e93a2938931f587d626


Parents : 627b6ef
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-12T17:31:42-05:00

feat(pip-rns): introduce pip-rns integration scripts and aliases for Reticulum packages

Changes
Diff

diff --git a/meshchatx/src/backend/recovery/health_monitor.py b/meshchatx/src/backend/recovery/health_monitor.py
index 204e150a..f26c3cb6 100644
--- a/meshchatx/src/backend/recovery/health_monitor.py
+++ b/meshchatx/src/backend/recovery/health_monitor.py
@@ -6,7 +6,7 @@ Runs as a daemon thread on a 5-minute interval, reading in-memory
metrics (log entropy, error rate, memory) and emitting WebSocket
warnings when anomalous trends are detected.
-No database queries are made in the monitor loop — all reads come
+No database queries are made in the monitor loop - all reads come
from in-memory deques kept by PersistentLogHandler and psutil.
"""

diff --git a/scripts/build/fetch_reticulum_manual.py b/scripts/build/fetch_reticulum_manual.py
index e53d4bf1..eecdb645 100755
--- a/scripts/build/fetch_reticulum_manual.py
+++ b/scripts/build/fetch_reticulum_manual.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+# SPDX-License-Identifier: 0BSD
"""Fetch the Reticulum manual at build time and stage it for bundling.
The downloaded archive is extracted into ``meshchatx/public/reticulum-docs-bundled/current``
@@ -11,6 +12,9 @@ Usage::
python scripts/build/fetch_reticulum_manual.py [--source URL] [--dest DIR]
[--force] [--include-pdf]
+Sources may be HTTPS ZIP URLs, local directories that contain a ``docs/`` tree, or
+``rns://`` rngit remotes (requires ``git`` and ``git-remote-rns``).
+
By default the upstream PDF/EPUB copies of the manual are excluded from the
bundle because the in-app viewer only renders the HTML version. Pass
``--include-pdf`` (or set ``MESHCHATX_DOCS_INCLUDE_PDF=1``) to keep them.
@@ -19,6 +23,8 @@ Environment variables::
MESHCHATX_RETICULUM_DOCS_URL Override the default source URL (single value).
MESHCHATX_RETICULUM_DOCS_DEST Override the destination directory.
+ MESHCHATX_RETICULUM_DOCS_VIA_RNS If set, prefer the default rngit website remote.
+ MESHCHATX_RETICULUM_DOCS_REF Git ref for ``rns://`` clones (default HEAD).
MESHCHATX_SKIP_DOCS_FETCH If set to ``1``/``true``, exit without fetching.
MESHCHATX_DOCS_INCLUDE_PDF If set to ``1``/``true``, include PDF/EPUB.
"""
@@ -31,16 +37,25 @@ import json
import logging
import os
import shutil
+import subprocess
import sys
+import tempfile
import urllib.error
import urllib.request
import zipfile
from datetime import UTC, datetime
from pathlib import Path
+_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
+if str(_SCRIPTS_DIR) not in sys.path:
+ sys.path.insert(0, str(_SCRIPTS_DIR))
+
+from pip_rns_remotes import DEFAULT_WEBSITE_REMOTE # noqa: E402
+
DEFAULT_SOURCES = (
"https://codeload.github.com/markqvist/reticulum_website/zip/refs/heads/master",
)
+DEFAULT_RNS_SOURCE = DEFAULT_WEBSITE_REMOTE
DEFAULT_DEST = (
Path(__file__).resolve().parent.parent.parent
@@ -62,6 +77,10 @@ def _is_truthy(value: str | None) -> bool:
return value is not None and value.strip().lower() in {"1", "true", "yes", "on"}
+def _is_rns_source(source: str) -> bool:
+ return source.strip().lower().startswith("rns://")
+
+
def _download(url: str, timeout: float) -> bytes:
logging.info("Downloading Reticulum manual from %s", url)
req = urllib.request.Request(
@@ -128,30 +147,181 @@ def _extract(
return extracted, skipped_binary
+def _extract_from_docs_dir(
+ docs_dir: Path,
+ dest: Path,
+ include_pdf: bool = False,
+) -> tuple[int, int]:
+ """Copy a local ``docs/`` tree into ``dest``."""
+ if not docs_dir.is_dir():
+ raise ValueError(f"docs directory missing: {docs_dir}")
+ extracted = 0
+ skipped_binary = 0
+ for path in docs_dir.rglob("*"):
+ if not path.is_file():
+ continue
+ rel = path.relative_to(docs_dir).as_posix()
+ if not rel or ".." in rel.split("/"):
+ continue
+ if not include_pdf and rel.lower().endswith(EXTRA_BINARY_SUFFIXES):
+ skipped_binary += 1
+ continue
+ target = dest / rel
+ try:
+ target.relative_to(dest)
+ except ValueError:
+ continue
+ target.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(path, target)
+ extracted += 1
+ return extracted, skipped_binary
+
+
+def _find_docs_dir(root: Path) -> Path:
+ direct = root / "docs"
+ if direct.is_dir():
+ return direct
+ matches = [p for p in root.rglob("docs") if p.is_dir()]
+ for candidate in matches:
+ if (candidate / "index.html").is_file() or (candidate / "manual").is_dir():
+ return candidate
+ if matches:
+ return matches[0]
+ raise ValueError(f"no docs/ directory found under {root}")
+
+
+def _run_git(args: list[str], *, cwd: Path | None, timeout: float) -> None:
+ try:
+ completed = subprocess.run(
+ args,
+ cwd=str(cwd) if cwd else None,
+ check=False,
+ capture_output=True,
+ timeout=timeout,
+ )
+ except subprocess.TimeoutExpired as exc:
+ raise ValueError(f"git timed out: {' '.join(args)}") from exc
+ if completed.returncode != 0:
+ err = (completed.stderr or b"").decode("utf-8", errors="replace")[:500]
+ raise ValueError(f"git failed ({completed.returncode}): {err or args}")
+
+
+def _clone_rns_docs(
+ remote: str,
+ *,
+ timeout: float,
+ ref: str,
+) -> Path:
+ """Clone an ``rns://`` website repo sparsely and return its ``docs/`` path."""
+ if shutil.which("git") is None:
+ raise ValueError("git is required for rns:// docs sources")
+ if shutil.which("git-remote-rns") is None:
+ raise ValueError("git-remote-rns is required for rns:// docs sources")
+
+ work = Path(tempfile.mkdtemp(prefix="meshchatx-rns-docs-"))
+ try:
+ logging.info("Cloning Reticulum website from %s (ref=%s)", remote, ref)
+ _run_git(
+ [
+ "git",
+ "clone",
+ "--filter=blob:none",
+ "--sparse",
+ "--no-checkout",
+ remote,
+ str(work),
+ ],
+ cwd=None,
+ timeout=timeout,
+ )
+ _run_git(
+ ["git", "sparse-checkout", "set", "--no-cone", "--", "docs"],
+ cwd=work,
+ timeout=min(60.0, timeout),
+ )
+ fetch_ref = ref if ref and ref != "HEAD" else "HEAD"
+ _run_git(
+ ["git", "fetch", "--depth", "1", "origin", fetch_ref],
+ cwd=work,
+ timeout=timeout,
+ )
+ _run_git(
+ ["git", "checkout", "FETCH_HEAD", "--", "docs"],
+ cwd=work,
+ timeout=min(120.0, timeout),
+ )
+ return _find_docs_dir(work)
+ except Exception:
+ shutil.rmtree(work, ignore_errors=True)
+ raise
+
+
+def _stage_from_local_or_rns(
+ source: str,
+ dest: Path,
+ *,
+ timeout: float,
+ include_pdf: bool,
+ ref: str,
+) -> tuple[int, int, str]:
+ """Return extracted counts and a cleanup workdir path (may be empty)."""
+ cleanup = ""
+ local = Path(source)
+ if local.exists() and local.is_dir():
+ docs_dir = _find_docs_dir(local)
+ extracted, skipped = _extract_from_docs_dir(
+ docs_dir, dest, include_pdf=include_pdf
+ )
+ return extracted, skipped, cleanup
+
+ if _is_rns_source(source):
+ docs_dir = _clone_rns_docs(source.strip(), timeout=timeout, ref=ref)
+ cleanup = str(docs_dir.parent)
+ try:
+ extracted, skipped = _extract_from_docs_dir(
+ docs_dir, dest, include_pdf=include_pdf
+ )
+ finally:
+ shutil.rmtree(cleanup, ignore_errors=True)
+ cleanup = ""
+ return extracted, skipped, cleanup
+
+ raise ValueError(f"unsupported local/rns source: {source}")
+
+
def _write_bundle_manifest(
*,
source_url: str,
dest: Path,
extracted: int,
skipped_binary: int,
+ manifest_path: Path,
) -> None:
repo_root = Path(__file__).resolve().parent.parent.parent
try:
dest_value = str(dest.resolve().relative_to(repo_root))
except ValueError:
dest_value = str(dest.resolve())
+
+ source_value = source_url
+ if source_url.startswith("/"):
+ try:
+ source_value = str(Path(source_url).resolve().relative_to(repo_root))
+ except ValueError:
+ pass
+
payload = {
- "source_url": source_url,
+ "source_url": source_value,
"dest": dest_value,
"fetched_utc": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"html_files": extracted,
"skipped_binary_files": skipped_binary,
}
- BUNDLE_MANIFEST_PATH.write_text(
+ manifest_path.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
- logging.info("Wrote bundle manifest to %s", BUNDLE_MANIFEST_PATH)
+ logging.info("Wrote bundle manifest to %s", manifest_path)
def fetch_manual(
@@ -160,6 +330,8 @@ def fetch_manual(
timeout: float = 120.0,
force: bool = False,
include_pdf: bool = False,
+ ref: str = "HEAD",
+ manifest_path: Path | None = None,
) -> int:
if dest.exists() and any(dest.iterdir()) and not force:
logging.info(
@@ -170,39 +342,56 @@ def fetch_manual(
return 0
last_error: Exception | None = None
- archive: zipfile.ZipFile | None = None
- docs_prefix: str | None = None
+ extracted = 0
+ skipped_binary = 0
source_url: str | None = None
+
for url in sources:
try:
+ if dest.exists():
+ shutil.rmtree(dest)
+ dest.mkdir(parents=True, exist_ok=True)
+
+ if _is_rns_source(url) or Path(url).exists():
+ extracted, skipped_binary, _cleanup = _stage_from_local_or_rns(
+ url,
+ dest,
+ timeout=timeout,
+ include_pdf=include_pdf,
+ ref=ref,
+ )
+ source_url = url
+ break
+
data = _download(url, timeout)
archive, docs_prefix = _resolve_docs_root(data)
+ try:
+ extracted, skipped_binary = _extract(
+ archive,
+ docs_prefix,
+ dest,
+ include_pdf=include_pdf,
+ )
+ finally:
+ archive.close()
source_url = url
break
- except (urllib.error.URLError, OSError, ValueError, zipfile.BadZipFile) as exc:
+ except (
+ urllib.error.URLError,
+ OSError,
+ ValueError,
+ zipfile.BadZipFile,
+ ) as exc:
logging.warning("Failed to fetch %s: %s", url, exc)
last_error = exc
- archive = None
- docs_prefix = None
+ if dest.exists():
+ shutil.rmtree(dest, ignore_errors=True)
- if archive is None or docs_prefix is None:
+ if source_url is None:
raise SystemExit(
f"Could not download Reticulum manual from any source: {last_error}",
)
- try:
- if dest.exists():
- shutil.rmtree(dest)
- dest.mkdir(parents=True, exist_ok=True)
- extracted, skipped_binary = _extract(
- archive,
- docs_prefix,
- dest,
- include_pdf=include_pdf,
- )
- finally:
- archive.close()
-
if extracted == 0:
raise SystemExit("Archive contained no docs/ files to extract")
@@ -214,13 +403,13 @@ def fetch_manual(
)
logging.info("Extracted %d files to %s", extracted, dest)
- if source_url:
- _write_bundle_manifest(
- source_url=source_url,
- dest=dest,
- extracted=extracted,
- skipped_binary=skipped_binary,
- )
+ _write_bundle_manifest(
+ source_url=source_url,
+ dest=dest,
+ extracted=extracted,
+ skipped_binary=skipped_binary,
+ manifest_path=manifest_path or BUNDLE_MANIFEST_PATH,
+ )
return extracted
@@ -231,8 +420,8 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
action="append",
default=None,
help=(
- "URL of a Reticulum website ZIP. May be passed multiple times to provide "
- "fallbacks. Defaults to the canonical upstream sources."
+ "HTTPS ZIP URL, local checkout path, or rns:// remote. May be passed "
+ "multiple times for fallbacks. Defaults to the canonical upstream sources."
),
)
parser.add_argument(
@@ -245,13 +434,27 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
"--timeout",
type=float,
default=120.0,
- help="HTTP timeout in seconds.",
+ help="HTTP / git timeout in seconds.",
+ )
+ parser.add_argument(
+ "--ref",
+ default=os.environ.get("MESHCHATX_RETICULUM_DOCS_REF", "HEAD"),
+ help="Git ref for rns:// clones (default HEAD or MESHCHATX_RETICULUM_DOCS_REF).",
)
parser.add_argument(
"--force",
action="store_true",
help="Re-fetch even if the destination already exists.",
)
+ parser.add_argument(
+ "--via-rns",
+ action="store_true",
+ default=_is_truthy(os.environ.get("MESHCHATX_RETICULUM_DOCS_VIA_RNS")),
+ help=(
+ "Prefer the default rngit website remote "
+ f"({DEFAULT_RNS_SOURCE}). Also set MESHCHATX_RETICULUM_DOCS_VIA_RNS=1."
+ ),
+ )
parser.add_argument(
"--include-pdf",
action="store_true",
@@ -289,6 +492,8 @@ def main(argv: list[str] | None = None) -> int:
env_url = os.environ.get("MESHCHATX_RETICULUM_DOCS_URL")
if env_url:
sources.append(env_url)
+ if args.via_rns:
+ sources.insert(0, DEFAULT_RNS_SOURCE)
if not sources:
sources = list(DEFAULT_SOURCES)
@@ -313,6 +518,7 @@ def main(argv: list[str] | None = None) -> int:
timeout=args.timeout,
force=args.force,
include_pdf=args.include_pdf,
+ ref=args.ref,
)
return 0

diff --git a/scripts/ci/github-draft-release-upload-assets.sh b/scripts/ci/github-draft-release-upload-assets.sh
index 69ea7be1..d3111b62 100644
--- a/scripts/ci/github-draft-release-upload-assets.sh
+++ b/scripts/ci/github-draft-release-upload-assets.sh
@@ -93,17 +93,17 @@ mapfile -t files < <(find "$STAGE" -type f)
{
if [[ "$TAG" == nightly-* ]]; then
- echo "**Nightly release** — automated daily snapshot from \`dev\`. Not a stable release; use tagged production releases for daily use."
+ echo "**Nightly release** - automated daily snapshot from \`dev\`. Not a stable release, use tagged production releases for daily use."
echo
echo "Commit: \`${GITHUB_SHA:-unknown}\`"
echo
elif [[ "$TAG" == preview-dev-* ]]; then
- echo "**Preview release (dev)** — automated snapshot from \`dev\`. Not a stable release; use tagged production releases for daily use."
+ echo "**Preview release (dev)** - automated snapshot from \`dev\`. Not a stable release, use tagged production releases for daily use."
echo
echo "Commit: \`${GITHUB_SHA:-unknown}\`"
echo
elif [[ "$TAG" == preview-* ]]; then
- echo "**Preview release** — automated snapshot from \`master\`. Not a stable release; use tagged production releases for daily use."
+ echo "**Preview release** - automated snapshot from \`master\`. Not a stable release, use tagged production releases for daily use."
echo
echo "Commit: \`${GITHUB_SHA:-unknown}\`"
echo

diff --git a/scripts/ci/priv.sh b/scripts/ci/priv.sh
index 043ce54b..3c63d28e 100644
--- a/scripts/ci/priv.sh
+++ b/scripts/ci/priv.sh
@@ -1,5 +1,5 @@
# shellcheck shell=sh
-# Sourced by scripts/ci/*.sh — run commands as root when sudo is missing (e.g. Docker, act).
+# Sourced by scripts/ci/*.sh - run commands as root when sudo is missing (e.g. Docker, act).
# Usage: . "$(dirname "$0")/priv.sh"
run_priv() {

diff --git a/scripts/docker-bake-lxst-filterlib-musl.py b/scripts/docker-bake-lxst-filterlib-musl.py
index 7d6abbfb..6b227b45 100644
--- a/scripts/docker-bake-lxst-filterlib-musl.py
+++ b/scripts/docker-bake-lxst-filterlib-musl.py
@@ -29,7 +29,7 @@ def main() -> int:
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ""
target = pkg / f"filterlib{ext_suffix}"
- import LXST.Filters # noqa: F401 — triggers cffi verify when needed
+ import LXST.Filters # noqa: F401 - triggers cffi verify when needed
candidates = sorted(
pkg.glob("__pycache__/_cffi__*.cpython-*-linux-musl.so"),

diff --git a/scripts/mutation/run.mjs b/scripts/mutation/run.mjs
index d5135c69..e05c43c7 100755
--- a/scripts/mutation/run.mjs
+++ b/scripts/mutation/run.mjs
@@ -62,7 +62,7 @@ function parseArgs(argv) {
}
function printHelp() {
- process.stdout.write(`MeshMut — in-repo JavaScript mutation testing
+ process.stdout.write(`MeshMut - in-repo JavaScript mutation testing
Usage:
node scripts/mutation/run.mjs [options]

diff --git a/scripts/pip-rns-deps.sh b/scripts/pip-rns-deps.sh
new file mode 100755
index 00000000..f9341379
--- /dev/null
+++ b/scripts/pip-rns-deps.sh
@@ -0,0 +1,196 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: 0BSD
+# Install rns / lxmf / lxst into the project uv environment via pip-rns (rngit).
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Install Reticulum Python packages over RNS using pip-rns.
+
+Requires a working Reticulum stack (rns already importable enough for mesh
+pathfinding), git, and git-remote-rns / rngit tooling. First-time bootstrap of
+rns itself still needs clearnet, a local wheel, or an existing install.
+
+Usage:
+ bash scripts/pip-rns-deps.sh [options] [package ...]
+
+Options:
+ -h, --help Show this help
+ --from-release Prefer rngit release wheels (--from-release)
+ --ref REF Pass --ref REF to each pip-rns install
+ --verify IDENTITY Require release signature (--verify)
+ --editable Editable install
+ --use-cache Pass --use-cache to pip-rns
+ --skip-ensure Do not try to install pip-rns if missing
+ --dry-run Print commands only
+
+Default packages: rns lxmf lxst
+
+Environment:
+ PIP_RNS_CONFIG Config dir with aliases (default: scripts/pip-rns)
+ MESHCHATX_PIP_RNS_FROM_RELEASE Set to 1 to imply --from-release
+ MESHCHATX_PIP_RNS_REF Default --ref when not passed on CLI
+ MESHCHATX_PIP_RNS_VERIFY Default --verify identity hash
+ MESHCHATX_PIP_RNS_PACKAGES Space-separated package list override
+EOF
+}
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "${ROOT_DIR}"
+
+CONFIG_DIR="${PIP_RNS_CONFIG:-${ROOT_DIR}/scripts/pip-rns}"
+export PIP_RNS_CONFIG="${CONFIG_DIR}"
+
+FROM_RELEASE=0
+REF="${MESHCHATX_PIP_RNS_REF:-}"
+VERIFY="${MESHCHATX_PIP_RNS_VERIFY:-}"
+EDITABLE=0
+USE_CACHE=0
+SKIP_ENSURE=0
+DRY_RUN=0
+PACKAGES=()
+
+if [[ "${MESHCHATX_PIP_RNS_FROM_RELEASE:-}" == "1" ]]; then
+ FROM_RELEASE=1
+fi
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --from-release)
+ FROM_RELEASE=1
+ shift
+ ;;
+ --ref)
+ REF="${2:-}"
+ shift 2
+ ;;
+ --verify)
+ VERIFY="${2:-}"
+ shift 2
+ ;;
+ --editable)
+ EDITABLE=1
+ shift
+ ;;
+ --use-cache)
+ USE_CACHE=1
+ shift
+ ;;
+ --skip-ensure)
+ SKIP_ENSURE=1
+ shift
+ ;;
+ --dry-run)
+ DRY_RUN=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ echo "Unknown option: $1" >&2
+ usage >&2
+ exit 2
+ ;;
+ *)
+ PACKAGES+=("$1")
+ shift
+ ;;
+ esac
+done
+
+if [[ ${#PACKAGES[@]} -eq 0 ]]; then
+ if [[ -n "${MESHCHATX_PIP_RNS_PACKAGES:-}" ]]; then
+ # shellcheck disable=SC2206
+ PACKAGES=(${MESHCHATX_PIP_RNS_PACKAGES})
+ else
+ PACKAGES=(rns lxmf lxst)
+ fi
+fi
+
+if [[ ! -f "${CONFIG_DIR}/aliases" ]]; then
+ echo "Missing aliases file: ${CONFIG_DIR}/aliases" >&2
+ exit 1
+fi
+
+run_cmd() {
+ if [[ "${DRY_RUN}" -eq 1 ]]; then
+ printf '+'
+ printf ' %q' "$@"
+ printf '\n'
+ return 0
+ fi
+ "$@"
+}
+
+ensure_pip_rns() {
+ if command -v pip-rns >/dev/null 2>&1; then
+ return 0
+ fi
+ if [[ "${SKIP_ENSURE}" -eq 1 ]]; then
+ echo "pip-rns not found on PATH (and --skip-ensure was set)" >&2
+ exit 1
+ fi
+ echo "pip-rns not found, installing into project environment with uv..." >&2
+ if command -v uv >/dev/null 2>&1; then
+ run_cmd uv pip install pip-rns
+ else
+ run_cmd python3 -m pip install pip-rns
+ fi
+ if ! command -v pip-rns >/dev/null 2>&1; then
+ if command -v uv >/dev/null 2>&1; then
+ PIP_RNS_BIN=(uv run pip-rns)
+ return 0
+ fi
+ echo "pip-rns still not on PATH after install" >&2
+ exit 1
+ fi
+}
+
+PIP_RNS_BIN=(pip-rns)
+if [[ "${DRY_RUN}" -eq 1 ]]; then
+ :
+else
+ ensure_pip_rns
+fi
+
+if [[ "${DRY_RUN}" -eq 0 ]]; then
+ echo "Note: Installing packages over RNS can be slow and use significant mesh bandwidth." >&2
+fi
+
+EXTRA_ARGS=()
+if [[ "${FROM_RELEASE}" -eq 1 ]]; then
+ EXTRA_ARGS+=(--from-release)
+fi
+if [[ -n "${REF}" ]]; then
+ EXTRA_ARGS+=(--ref "${REF}")
+fi
+if [[ -n "${VERIFY}" ]]; then
+ EXTRA_ARGS+=(--verify "${VERIFY}")
+fi
+if [[ "${EDITABLE}" -eq 1 ]]; then
+ EXTRA_ARGS+=(--editable)
+fi
+if [[ "${USE_CACHE}" -eq 1 ]]; then
+ EXTRA_ARGS+=(--use-cache)
+fi
+
+for pkg in "${PACKAGES[@]}"; do
+ echo "pip-rns install --uv ${pkg}${EXTRA_ARGS[*]:+ ${EXTRA_ARGS[*]}}"
+ run_cmd "${PIP_RNS_BIN[@]}" install --uv "${pkg}" "${EXTRA_ARGS[@]}"
+done
+
+if [[ "${DRY_RUN}" -eq 0 ]]; then
+ if command -v uv >/dev/null 2>&1; then
+ run_cmd uv run python scripts/patch_lxst_pyogg_ogg_ctypes.py
+ else
+ run_cmd python3 scripts/patch_lxst_pyogg_ogg_ctypes.py
+ fi
+fi
+
+echo "Done. Installed via pip-rns: ${PACKAGES[*]}"

diff --git a/scripts/pip-rns/aliases b/scripts/pip-rns/aliases
new file mode 100644
index 00000000..ff8c12dc
--- /dev/null
+++ b/scripts/pip-rns/aliases
@@ -0,0 +1,7 @@
+# MeshChatX default pip-rns aliases for upstream Reticulum packages over rngit.
+# Format: name=identity_hex/group/repo
+# Override with PIP_RNS_CONFIG pointing at another directory that contains aliases.
+rns=7649a50d84610232d1416b41d2896aff/reticulum/reticulum
+lxmf=7649a50d84610232d1416b41d2896aff/reticulum/lxmf
+lxst=7649a50d84610232d1416b41d2896aff/reticulum/lxst
+website=7649a50d84610232d1416b41d2896aff/reticulum/website

diff --git a/scripts/pip_rns_remotes.py b/scripts/pip_rns_remotes.py
new file mode 100644
index 00000000..78dd3056
--- /dev/null
+++ b/scripts/pip_rns_remotes.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: 0BSD
+"""Shared remotes and helpers for optional pip-rns / rngit tooling."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+PIP_RNS_CONFIG_DIR = REPO_ROOT / "scripts" / "pip-rns"
+ALIASES_PATH = PIP_RNS_CONFIG_DIR / "aliases"
+
+# Upstream markqvist rngit remotes (destination hash + group/repo).
+DEFAULT_RNS_IDENTITY = "7649a50d84610232d1416b41d2896aff"
+DEFAULT_GROUP = "reticulum"
+
+DEFAULT_PACKAGE_ALIASES = {
+ "rns": f"{DEFAULT_RNS_IDENTITY}/{DEFAULT_GROUP}/reticulum",
+ "lxmf": f"{DEFAULT_RNS_IDENTITY}/{DEFAULT_GROUP}/lxmf",
+ "lxst": f"{DEFAULT_RNS_IDENTITY}/{DEFAULT_GROUP}/lxst",
+}
+
+DEFAULT_WEBSITE_REMOTE = f"rns://{DEFAULT_RNS_IDENTITY}/{DEFAULT_GROUP}/website"
+
+DEFAULT_INSTALL_PACKAGES = ("rns", "lxmf", "lxst")
+
+
+def parse_aliases(path: Path | None = None) -> dict[str, str]:
+ """Parse a pip-rns aliases file into ``name -> identity/group/repo``."""
+ target = path or ALIASES_PATH
+ result: dict[str, str] = {}
+ if not target.is_file():
+ return dict(DEFAULT_PACKAGE_ALIASES)
+ for raw in target.read_text(encoding="utf-8").splitlines():
+ line = raw.strip()
+ if not line or line.startswith("#"):
+ continue
+ if "=" not in line:
+ continue
+ name, value = line.split("=", 1)
+ name = name.strip()
+ value = value.strip()
+ if name and value:
+ result[name] = value
+ return result
+
+
+def remote_url(alias_or_path: str, aliases: dict[str, str] | None = None) -> str:
+ """Return an ``rns://`` URL for an alias name or raw ``identity/group/repo``."""
+ table = aliases if aliases is not None else parse_aliases()
+ raw = table.get(alias_or_path, alias_or_path).strip()
+ if raw.lower().startswith("rns://"):
+ return raw
+ return f"rns://{raw.lstrip('/')}"
+
+
+def website_docs_source(aliases: dict[str, str] | None = None) -> str:
+ """Preferred ``rns://`` source for the Reticulum website/manual repo."""
+ table = aliases if aliases is not None else parse_aliases()
+ if "website" in table:
+ return remote_url("website", table)
+ return DEFAULT_WEBSITE_REMOTE

diff --git a/tests/backend/test_pip_rns_integration.py b/tests/backend/test_pip_rns_integration.py
new file mode 100644
index 00000000..784c5478
--- /dev/null
+++ b/tests/backend/test_pip_rns_integration.py
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Tests for optional pip-rns / rngit dependency helpers."""
+
+from __future__ import annotations
+
+import importlib.util
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+_REPO = Path(__file__).resolve().parents[2]
+_SCRIPT = _REPO / "scripts" / "pip-rns-deps.sh"
+_ALIASES = _REPO / "scripts" / "pip-rns" / "aliases"
+_REMOTES = _REPO / "scripts" / "pip_rns_remotes.py"
+_FETCH = _REPO / "scripts" / "build" / "fetch_reticulum_manual.py"
+
+
+def _load_module(path: Path, name: str):
+ spec = importlib.util.spec_from_file_location(name, path)
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def test_pip_rns_deps_script_exists_and_is_executable():
+ assert _SCRIPT.is_file()
+ assert os.access(_SCRIPT, os.X_OK)
+
+
+def test_pip_rns_deps_bash_syntax():
+ subprocess.run(["bash", "-n", str(_SCRIPT)], check=True)
+
+
+def test_pip_rns_deps_help():
+ proc = subprocess.run(
+ ["bash", str(_SCRIPT), "--help"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ assert proc.returncode == 0
+ assert "pip-rns" in proc.stdout
+
+
+def test_pip_rns_deps_dry_run():
+ proc = subprocess.run(
+ ["bash", str(_SCRIPT), "--dry-run", "--skip-ensure"],
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=str(_REPO),
+ )
+ assert proc.returncode == 0
+ out = proc.stdout + proc.stderr
+ assert "pip-rns install --uv rns" in out
+ assert "pip-rns install --uv lxmf" in out
+ assert "pip-rns install --uv lxst" in out
+
+
+def test_aliases_file_has_markqvist_remotes():
+ text = _ALIASES.read_text(encoding="utf-8")
+ assert "7649a50d84610232d1416b41d2896aff/reticulum/reticulum" in text
+ assert "7649a50d84610232d1416b41d2896aff/reticulum/lxmf" in text
+ assert "7649a50d84610232d1416b41d2896aff/reticulum/lxst" in text
+ assert "7649a50d84610232d1416b41d2896aff/reticulum/website" in text
+
+
+def test_pip_rns_remotes_helpers():
+ remotes = _load_module(_REMOTES, "pip_rns_remotes_under_test")
+ aliases = remotes.parse_aliases(_ALIASES)
+ assert aliases["rns"].endswith("/reticulum/reticulum")
+ assert remotes.remote_url("lxmf", aliases).startswith("rns://")
+ assert remotes.website_docs_source(aliases).startswith("rns://")
+ assert "website" in remotes.website_docs_source(aliases)
+
+
+def test_fetch_manual_from_local_docs_tree(tmp_path):
+ fetch = _load_module(_FETCH, "fetch_reticulum_manual_under_test")
+ src = tmp_path / "website"
+ docs = src / "docs"
+ docs.mkdir(parents=True)
+ (docs / "index.html").write_text("<html>ok</html>", encoding="utf-8")
+ (docs / "manual").mkdir()
+ (docs / "manual" / "index.html").write_text("<html>m</html>", encoding="utf-8")
+ (docs / "manual.pdf").write_bytes(b"%PDF")
+
+ dest = tmp_path / "out"
+ manifest = tmp_path / "manifest.json"
+ count = fetch.fetch_manual(
+ sources=[str(src)],
+ dest=dest,
+ force=True,
+ include_pdf=False,
+ manifest_path=manifest,
+ )
+ assert count == 2
+ assert (dest / "index.html").is_file()
+ assert (dest / "manual" / "index.html").is_file()
+ assert not (dest / "manual.pdf").exists()
+ assert manifest.is_file()
+
+
+def test_taskfile_has_pip_rns_targets():
+ text = (_REPO / "Taskfile.yml").read_text(encoding="utf-8")
+ assert "deps:backend:rns:" in text
+ assert "docs:rns:" in text
+ assert "scripts/pip-rns-deps.sh" in text


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────